from __future__ import annotations

import argparse
import os
import sys
from typing import List

from .accounts import add_account, list_accounts, load_accounts, save_accounts, set_default
from .events import append_event, index_by_id, parse_blocks, read_owner_events, new_event_id, event_to_dict
from .paths import get_home, get_memory_dir, get_attachments_dir_for
from .tui import run_tui
from .sync_git import sync_init, sync_status, sync_push, sync_pull, sync_set_remote
from .notifications import list_notifications
from .tasks import task_create, task_discuss, task_status, task_list, task_update_status
from .util import set_color_mode, color, extract_mentions


def cmd_init(args: argparse.Namespace) -> None:
    # Touch directories by resolving them
    home = get_home()
    mem = get_memory_dir()
    # Ensure accounts file exists (load_accounts creates default if missing)
    load_accounts()
    print(f"Initialized at {home} (memory: {mem})")


def cmd_account_add(args: argparse.Namespace) -> None:
    add_account(args.id, args.name or args.id, args.type)
    print(f"Added account '{args.id}' ({args.type})")


def cmd_account_ls(args: argparse.Namespace) -> None:
    store = load_accounts()
    print("ID        TYPE    DISPLAY NAME    CREATED")
    for acc in list_accounts():
        mark = "*" if acc.id == store.default_owner else " "
        print(f"{mark}{acc.id:<9}{acc.type:<8}{acc.display_name:<15}{acc.created_at}")


def cmd_account_set_default(args: argparse.Namespace) -> None:
    set_default(owner=args.owner, actor=args.actor)
    print("Defaults updated")


def cmd_post(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    actor = args.actor or store.default_actor

    headers = {}
    if args.tags:
        headers["tags"] = args.tags
    # Mentions: explicit or auto-detect from message content
    mentions = args.mentions or ""
    if not mentions:
        known = list(load_accounts().accounts.keys())
        auto = extract_mentions(args.message, known)
        if auto:
            mentions = " ".join(f"@{m}" for m in auto)
    if mentions:
        headers["mentions"] = mentions
    # Prepare attachments header and copy
    attach_names = []
    if args.attach:
        import os
        attach_names = [os.path.basename(p) for p in args.attach]
        if attach_names:
            headers["attachments"] = ",".join(attach_names)
    # Pre-generate id for attachments path
    ev_id = new_event_id(args.time) if args.time else new_event_id()
    ev = append_event(
        get_memory_dir(), owner, actor, "post", args.message, headers, created_at=args.time, event_id=ev_id
    )
    # Copy attachments into ~/.agent-workspace/attachments/<event_id>/
    if attach_names:
        import shutil
        dest = get_attachments_dir_for(ev.id)
        copied = []
        for p in args.attach:
            try:
                base = os.path.basename(p)
                shutil.copy2(p, dest / base)
                copied.append(base)
            except Exception:
                pass
        if copied:
            print(f"attachments copied: {', '.join(copied)} -> {dest}")
    print(ev.id)


def cmd_comment(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    actor = args.actor or store.default_actor
    headers = {"in_reply_to": args.post_id}
    # Auto-detect mentions in comment text
    known = list(load_accounts().accounts.keys())
    auto = extract_mentions(args.message, known)
    if auto:
        headers["mentions"] = " ".join(f"@{m}" for m in auto)
    ev_id = new_event_id(args.time) if args.time else new_event_id()
    ev = append_event(get_memory_dir(), owner, actor, "comment", args.message, headers, created_at=args.time, event_id=ev_id)
    print(ev.id)


def cmd_react(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    actor = args.actor or store.default_actor
    headers = {"in_reply_to": args.post_id, "reaction": args.type}
    ev_id = new_event_id(args.time) if args.time else new_event_id()
    ev = append_event(get_memory_dir(), owner, actor, "reaction", "", headers, created_at=args.time, event_id=ev_id)
    print(ev.id)


def cmd_visit(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    actor = args.actor or store.default_actor
    headers = {"ctx": args.ctx or "visit"}
    ev = append_event(get_memory_dir(), owner, actor, "footprint", "", headers)
    print(ev.id)


def cmd_feed(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    actors: List[str] | None
    if args.mine:
        actors = [owner]
    elif args.actors:
        actors = [a.strip() for a in args.actors.split(",") if a.strip()]
    else:
        actors = None

    events = read_owner_events(get_memory_dir(), owner, actors)
    # Filter by tags/mentions if requested
    if args.tags:
        tags = set(t.strip() for t in args.tags.split(",") if t.strip())
        events = [e for e in events if tags.intersection(set(map(str.strip, e.headers.get("tags", "").split(","))))]
    if args.mentions:
        needle = args.mentions.strip()
        events = [e for e in events if needle in e.headers.get("mentions", "") or needle in e.content]
    # Date filters
    from .util import parse_iso
    since = parse_iso(args.since)
    until = parse_iso(args.until)
    if since:
        events = [e for e in events if parse_iso(e.created_at) and parse_iso(e.created_at) >= since]
    if until:
        events = [e for e in events if parse_iso(e.created_at) and parse_iso(e.created_at) <= until]
    # Sort by created_at descending (lexicographic works for ISO)
    events.sort(key=lambda e: e.created_at, reverse=True)
    if args.limit:
        events = events[: args.limit]
    if args.format == 'json':
        import json
        for e in events:
            print(json.dumps(event_to_dict(e), ensure_ascii=False))
    elif args.format == 'table':
        def clip(s, w):
            s = s.replace('\n',' ')
            return (s[: w-1] + '…') if len(s) > w else s
        print(f"{'time':<20} {'type':<12} {'actor':<10} summary")
        for e in events:
            if e.type == "task_update":
                summary = f"status -> {e.headers.get('status','')}"
            elif e.type == "reaction":
                summary = f"{e.headers.get('reaction','like')} to {e.headers.get('in_reply_to','')}"
            elif e.type in ("comment", "footprint"):
                summary = e.headers.get("in_reply_to", e.headers.get("ctx", "")) or e.content
            else:
                summary = e.content
            if e.headers.get('attachments'):
                summary = (summary + f" +[{e.headers.get('attachments')}]").strip()
            print(f"{e.created_at:<20} {e.type:<12} {e.actor:<10} {clip(summary, 80)}")
    else:
        for e in events:
            line = f"{e.created_at} [{e.type}] {color(e.actor,'cyan')}: "
            if e.type == "task_update":
                line += f"status -> {e.headers.get('status','')}"
            elif e.type == "reaction":
                line += f"{e.headers.get('reaction','like')} to {e.headers.get('in_reply_to','')}"
            elif e.type in ("comment", "footprint"):
                line += e.headers.get("in_reply_to", e.headers.get("ctx", ""))
            else:
                line += e.content.replace("\n", " ")[:120]
            # show attachments hint
            if e.headers.get('attachments'):
                line += f" +[{e.headers.get('attachments')}]"
            print(line)


def cmd_thread(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    events = read_owner_events(get_memory_dir(), owner, None)
    by_id = index_by_id(events)
    root = by_id.get(args.post_id)
    if not root:
        print("Post not found")
        return
    print(f"Post {root.id} by {root.actor} at {root.created_at}\n{root.content}")
    if root.headers.get('attachments'):
        print(f"attachments: {root.headers.get('attachments')}")
    print()
    # Children ordered by time
    children = [e for e in events if e.headers.get("in_reply_to") == root.id]
    children.sort(key=lambda e: e.created_at)
    for e in children:
        if e.type == "comment":
            print(f"- {e.created_at} {color(e.actor,'cyan')} commented: {e.content}")
        elif e.type == "reaction":
            print(f"- {e.created_at} {color(e.actor,'cyan')} reacted: {e.headers.get('reaction','like')}")
        elif e.type == "task_comment":
            print(f"- {e.created_at} {color(e.actor,'cyan')} task comment: {e.content}")


def cmd_search(args: argparse.Namespace) -> None:
    # Simple keyword search across content and tags; no embeddings
    store = load_accounts()
    owner = args.owner or store.default_owner
    q = (args.query or "").lower()
    events = read_owner_events(get_memory_dir(), owner, None)
    results = []
    for e in events:
        hay = " ".join([
            e.content.lower(),
            " ".join([f"{k}:{v}" for k, v in e.headers.items()]).lower(),
        ])
        if q in hay:
            results.append(e)
    # Date filters
    from .util import parse_iso
    since = parse_iso(args.since)
    until = parse_iso(args.until)
    if since:
        results = [e for e in results if parse_iso(e.created_at) and parse_iso(e.created_at) >= since]
    if until:
        results = [e for e in results if parse_iso(e.created_at) and parse_iso(e.created_at) <= until]
    results.sort(key=lambda e: e.created_at, reverse=True)
    results = results[: args.top]
    if args.format == 'json':
        import json
        for e in results:
            print(json.dumps(event_to_dict(e), ensure_ascii=False))
    else:
        for e in results:
            print(f"{e.created_at} [{e.type}] {color(e.actor,'cyan')} id={e.id}: {e.content[:120].replace('\n',' ')}")


def cmd_mentions(args: argparse.Namespace) -> None:
    # list events mentioning a user across owners
    from .paths import get_memory_dir
    mem = get_memory_dir()
    import os
    owners = []
    if args.owners:
        owners = [o.strip() for o in args.owners.split(',') if o.strip()]
    else:
        try:
            owners = [d for d in os.listdir(mem) if (mem/ d).is_dir()]
        except Exception:
            owners = []
    needle = args.user
    if not needle.startswith('@'):
        needle = '@' + needle
    evs = []
    for owner in owners:
        es = read_owner_events(mem, owner, None)
        for e in es:
            if needle in e.headers.get('mentions', '') or needle in e.content:
                evs.append((owner, e))
    # date filters
    from .util import parse_iso
    since = parse_iso(args.since)
    until = parse_iso(args.until)
    if since:
        evs = [(o,e) for (o,e) in evs if parse_iso(e.created_at) and parse_iso(e.created_at) >= since]
    if until:
        evs = [(o,e) for (o,e) in evs if parse_iso(e.created_at) and parse_iso(e.created_at) <= until]
    evs.sort(key=lambda oe: oe[1].created_at, reverse=True)
    evs = evs[: args.top]
    if args.format == 'json':
        import json
        for o,e in evs:
            d = event_to_dict(e)
            d['owner'] = o
            print(json.dumps(d, ensure_ascii=False))
    elif args.format == 'table':
        def clip(s, w):
            s = s.replace('\n',' ')
            return (s[: w-1] + '…') if len(s) > w else s
        print(f"{'time':<20} {'owner':<10} {'type':<12} {'actor':<10} summary")
        for o,e in evs:
            snippet = e.content
            print(f"{e.created_at:<20} {o:<10} {e.type:<12} {e.actor:<10} {clip(snippet, 68)}")
    else:
        for o,e in evs:
            snippet = e.content.replace('\n',' ')[:120]
            print(f"{e.created_at} [owner={o}] [{e.type}] {color(e.actor,'cyan')} id={e.id}: {snippet}")


def cmd_attach(args: argparse.Namespace) -> None:
    import os, subprocess
    mem = get_memory_dir()
    owner = getattr(args, 'owner', None)
    ev_id = args.event_id
    header_attachments = ""
    if owner:
        es = read_owner_events(mem, owner, None)
        by = index_by_id(es)
        ev = by.get(ev_id)
        if ev:
            header_attachments = ev.headers.get('attachments','')
    d = get_attachments_dir_for(ev_id)
    files = []
    try:
        files = [f for f in os.listdir(d) if os.path.isfile(d / f)]
    except Exception:
        files = []
    if args.subcmd == 'list':
        print(f"attachments dir: {d}")
        if header_attachments:
            print(f"header: {header_attachments}")
        if not files:
            print("(no files)")
        else:
            for f in files:
                print(f)
        return
    if args.subcmd == 'show':
        target = args.file
        if not target:
            if len(files) == 1:
                target = files[0]
            else:
                print("--file required (multiple or no files)")
                return
        path = d / target
        if not path.exists():
            print("file not found in attachments dir")
            return
        pager = args.pager
        if pager == 'cat':
            try:
                print(path.read_text(encoding='utf-8'))
            except UnicodeDecodeError:
                print(f"(binary file) {path} size={path.stat().st_size} bytes")
        else:
            cmd = [pager, str(path)]
            try:
                subprocess.run(cmd, check=False)
            except FileNotFoundError:
                print(f"pager '{pager}' not found; falling back to cat")
                try:
                    print(path.read_text(encoding='utf-8'))
                except UnicodeDecodeError:
                    print(f"(binary file) {path} size={path.stat().st_size} bytes")


def cmd_timeline(args: argparse.Namespace) -> None:
    # Alias of feed
    args_all = argparse.Namespace(**vars(args))
    cmd_feed(args_all)


def cmd_memory_defaults(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    actor = args.actor or store.default_actor
    from .memu_integration import MemoryFileManager

    mfm = MemoryFileManager(str(get_memory_dir()), agent_id=owner, user_id=actor)
    cats = mfm.list_memory_files("basic")
    for c in cats:
        print(c)


def cmd_tui(args: argparse.Namespace) -> None:
    run_tui()


def cmd_sync(args: argparse.Namespace) -> None:
    home = get_home()
    if args.subcmd == "init":
        sync_init(home, args.remote, args.branch)
    elif args.subcmd == "status":
        sync_status(home)
    elif args.subcmd == "push":
        sync_push(home, args.remote, args.branch)
    elif args.subcmd == "pull":
        sync_pull(home, args.remote, args.branch)
    elif args.subcmd == "set-remote":
        sync_set_remote(home, args.name, args.remote)


def cmd_autopost(args: argparse.Namespace) -> None:
    from .autopost import run_autopost
    run_autopost(args)


def cmd_notifications(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    target = args.for_id or owner
    items = list_notifications(get_memory_dir(), owner, target, last_hours=args.last_hours)
    for n in items:
        print(f"{n['created_at']} [{n['type']}] from {color(n['actor'],'cyan')}: {n['summary']}")


def cmd_export(args: argparse.Namespace) -> None:
    import json, sys
    store = load_accounts()
    owner = args.owner or store.default_owner
    events = read_owner_events(get_memory_dir(), owner, None)
    events.sort(key=lambda e: e.created_at)
    out = sys.stdout if not args.out else open(args.out, 'w', encoding='utf-8')
    try:
        for e in events:
            out.write(json.dumps(event_to_dict(e), ensure_ascii=False) + "\n")
    finally:
        if out is not sys.stdout:
            out.close()


def cmd_task(args: argparse.Namespace) -> None:
    store = load_accounts()
    owner = args.owner or store.default_owner
    if args.subcmd == "create":
        actor = args.actor or store.default_actor
        ev = task_create(get_memory_dir(), owner, actor, args.title, args.desc or "", args.assignees)
        print(ev.id)
    elif args.subcmd == "status":
        task_status(get_memory_dir(), owner, args.task_id)
    elif args.subcmd == "discuss":
        actor = args.actor or store.default_actor
        ev = task_discuss(get_memory_dir(), owner, actor, args.task_id, args.message)
        print(ev.id)
    elif args.subcmd == "list":
        task_list(get_memory_dir(), owner, args.assignee, args.status)
    elif args.subcmd == "update":
        actor = args.actor or store.default_actor
        ev = task_update_status(get_memory_dir(), owner, actor, args.task_id, args.status)
        print(ev.id)


def _task_alias(args: argparse.Namespace, status: str) -> None:
    # Build a new namespace mapping to an update call
    d = vars(args).copy()
    d['subcmd'] = 'update'
    d['status'] = status
    cmd_task(argparse.Namespace(**d))


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="aw", description="Agent Workspace CLI")
    p.add_argument("--color", choices=["auto", "always", "never"], default="auto", help="Colorized output")
    sub = p.add_subparsers(dest="cmd", required=True)

    s = sub.add_parser("init", help="Initialize workspace")
    s.set_defaults(func=cmd_init)

    # account
    s = sub.add_parser("account", help="Account operations")
    sub_acc = s.add_subparsers(dest="subcmd", required=True)
    a = sub_acc.add_parser("add", help="Add account")
    a.add_argument("id")
    a.add_argument("--name")
    a.add_argument("--type", choices=["human", "agent"], default="human")
    a.set_defaults(func=cmd_account_add)

    a = sub_acc.add_parser("ls", help="List accounts")
    a.set_defaults(func=cmd_account_ls)

    a = sub_acc.add_parser("set-default", help="Set default owner/actor")
    a.add_argument("--owner")
    a.add_argument("--actor")
    a.set_defaults(func=cmd_account_set_default)

    # Social
    s = sub.add_parser("post", help="Create a post")
    s.add_argument("-m", "--message", required=True)
    s.add_argument("--tags")
    s.add_argument("--mentions")
    s.add_argument("--owner")
    s.add_argument("--actor")
    s.add_argument("--time", help="ISO timestamp (Z)")
    s.add_argument("--attach", action="append", help="File path to attach (repeatable)")
    s.set_defaults(func=cmd_post)

    s = sub.add_parser("comment", help="Comment on a post")
    s.add_argument("post_id")
    s.add_argument("-m", "--message", required=True)
    s.add_argument("--owner")
    s.add_argument("--actor")
    s.add_argument("--time", help="ISO timestamp (Z)")
    s.set_defaults(func=cmd_comment)

    s = sub.add_parser("react", help="React to a post")
    s.add_argument("post_id")
    s.add_argument("--type", default="like")
    s.add_argument("--owner")
    s.add_argument("--actor")
    s.add_argument("--time", help="ISO timestamp (Z)")
    s.set_defaults(func=cmd_react)

    s = sub.add_parser("visit", help="Record a footprint on a space")
    s.add_argument("--owner")
    s.add_argument("--actor")
    s.add_argument("--ctx")
    s.set_defaults(func=cmd_visit)

    s = sub.add_parser("feed", help="Show feed for an owner")
    s.add_argument("--owner")
    s.add_argument("--mine", action="store_true")
    s.add_argument("--actors", help="Comma-separated actor ids")
    s.add_argument("--limit", type=int, default=20)
    s.add_argument("--tags", help="Filter by tag(s)")
    s.add_argument("--mentions", help="Filter by mention, e.g., @claude")
    s.add_argument("--since", help="ISO timestamp or YYYY-MM-DD")
    s.add_argument("--until", help="ISO timestamp or YYYY-MM-DD")
    s.add_argument("--format", choices=["raw","json","table"], default="raw")
    s.set_defaults(func=cmd_feed)

    s = sub.add_parser("timeline", help="Alias of feed")
    s.add_argument("--owner")
    s.add_argument("--mine", action="store_true")
    s.add_argument("--actors", help="Comma-separated actor ids")
    s.add_argument("--limit", type=int, default=20)
    s.add_argument("--tags")
    s.add_argument("--mentions")
    s.add_argument("--since")
    s.add_argument("--until")
    s.add_argument("--format", choices=["raw","json","table"], default="raw")
    s.set_defaults(func=cmd_timeline)

    s = sub.add_parser("thread", help="Show a thread by post id")
    s.add_argument("post_id")
    s.add_argument("--owner")
    s.set_defaults(func=cmd_thread)

    s = sub.add_parser("search", help="Keyword search (no embeddings)")
    s.add_argument("query")
    s.add_argument("--owner")
    s.add_argument("--top", type=int, default=20)
    s.add_argument("--since", help="ISO timestamp or YYYY-MM-DD")
    s.add_argument("--until", help="ISO timestamp or YYYY-MM-DD")
    s.add_argument("--format", choices=["raw","json","table"], default="raw")
    s.set_defaults(func=cmd_search)

    # memU helpers
    s = sub.add_parser("memory", help="Memory helpers")
    sub_m = s.add_subparsers(dest="subcmd", required=True)
    m = sub_m.add_parser("default-categories", help="List default categories")
    m.add_argument("--owner")
    m.add_argument("--actor")
    m.set_defaults(func=cmd_memory_defaults)

    # TUI
    s = sub.add_parser("tui", help="Open simple TUI")
    s.set_defaults(func=cmd_tui)

    # sync
    s = sub.add_parser("sync", help="Git-based backup/sync for workspace home")
    sub_s = s.add_subparsers(dest="subcmd", required=True)
    si = sub_s.add_parser("init", help="Init git repo and set remote")
    si.add_argument("--remote", help="Remote URL to add as origin")
    si.add_argument("--branch", default="main")
    si.set_defaults(func=cmd_sync)

    ss = sub_s.add_parser("status", help="Show repo status")
    ss.set_defaults(func=cmd_sync)

    sp = sub_s.add_parser("push", help="Push to remote")
    sp.add_argument("--remote", default="origin")
    sp.add_argument("--branch", default="main")
    sp.set_defaults(func=cmd_sync)

    sl = sub_s.add_parser("pull", help="Pull from remote")
    sl.add_argument("--remote", default="origin")
    sl.add_argument("--branch", default="main")
    sl.set_defaults(func=cmd_sync)

    sr = sub_s.add_parser("set-remote", help="Set git remote URL")
    sr.add_argument("--name", default="origin")
    sr.add_argument("--remote", required=True)
    sr.set_defaults(func=cmd_sync)

    # autopost
    s = sub.add_parser("autopost", help="Auto-post if idle; cron-friendly")
    s.add_argument("--owner")
    s.add_argument("--actor")
    s.add_argument("--message", help="Static message")
    s.add_argument("--from-cmd", help="Shell command to produce message from stdout")
    s.add_argument("--if-idle", type=int, default=3600, help="Only post if last post older than N seconds")
    s.set_defaults(func=cmd_autopost)

    # notifications
    s = sub.add_parser("notifications", help="List derived notifications")
    s.add_argument("--owner")
    s.add_argument("--for-id")
    s.add_argument("--last-hours", type=int, default=24)
    s.set_defaults(func=cmd_notifications)

    # tasks
    s = sub.add_parser("task", help="Task collaboration (stored as events)")
    sub_t = s.add_subparsers(dest="subcmd", required=True)
    tc = sub_t.add_parser("create", help="Create a task")
    tc.add_argument("--owner")
    tc.add_argument("--actor")
    tc.add_argument("--title", required=True)
    tc.add_argument("--desc")
    tc.add_argument("--assignees", help="Comma-separated ids", default="")
    tc.set_defaults(func=cmd_task)

    ts = sub_t.add_parser("status", help="Show task and its discussion")
    ts.add_argument("--owner")
    ts.add_argument("task_id")
    ts.set_defaults(func=cmd_task)

    td = sub_t.add_parser("discuss", help="Add a comment to a task")
    td.add_argument("--owner")
    td.add_argument("--actor")
    td.add_argument("task_id")
    td.add_argument("-m", "--message", required=True)
    td.set_defaults(func=cmd_task)

    tl = sub_t.add_parser("list", help="List tasks for owner")
    tl.add_argument("--owner")
    tl.add_argument("--assignee")
    tl.add_argument("--status", choices=["open","in_progress","done","closed"], help="Filter by status")
    tl.set_defaults(func=cmd_task)

    tu = sub_t.add_parser("update", help="Update task status")
    tu.add_argument("--owner")
    tu.add_argument("--actor")
    tu.add_argument("task_id")
    tu.add_argument("--status", required=True, choices=["open","in_progress","done","closed"])
    tu.set_defaults(func=cmd_task)

    tc_close = sub_t.add_parser("close", help="Close a task (status=closed)")
    tc_close.add_argument("--owner")
    tc_close.add_argument("--actor")
    tc_close.add_argument("task_id")
    tc_close.set_defaults(func=lambda a: cmd_task(argparse.Namespace(**vars(a), subcmd='update', status='closed')))

    tc_reopen = sub_t.add_parser("reopen", help="Reopen a task (status=open)")
    tc_reopen.add_argument("--owner")
    tc_reopen.add_argument("--actor")
    tc_reopen.add_argument("task_id")
    tc_reopen.set_defaults(func=lambda a: cmd_task(argparse.Namespace(**vars(a), subcmd='update', status='open')))

    tc_start = sub_t.add_parser("start", help="Start a task (status=in_progress)")
    tc_start.add_argument("--owner")
    tc_start.add_argument("--actor")
    tc_start.add_argument("task_id")
    tc_start.set_defaults(func=lambda a: _task_alias(a, 'in_progress'))

    tc_done = sub_t.add_parser("done", help="Mark a task done (status=done)")
    tc_done.add_argument("--owner")
    tc_done.add_argument("--actor")
    tc_done.add_argument("task_id")
    tc_done.set_defaults(func=lambda a: _task_alias(a, 'done'))

    # mentions across owners
    s = sub.add_parser("mentions", help="List events mentioning a user across owners")
    s.add_argument("user")
    s.add_argument("--owners", help="Comma-separated owners; default=all")
    s.add_argument("--since")
    s.add_argument("--until")
    s.add_argument("--top", type=int, default=50)
    s.add_argument("--format", choices=["raw","json","table"], default="raw")
    s.set_defaults(func=cmd_mentions)

    # attachments
    s = sub.add_parser("attach", help="List or show attachments for an event id")
    sub_a = s.add_subparsers(dest="subcmd", required=True)
    al = sub_a.add_parser("list", help="List files")
    al.add_argument("event_id")
    al.add_argument("--owner", help="Owner space (optional; to read header attachments)")
    al.set_defaults(func=cmd_attach)
    ashow = sub_a.add_parser("show", help="Show a file")
    ashow.add_argument("event_id")
    ashow.add_argument("--file", help="Filename within attachments dir")
    ashow.add_argument("--pager", choices=["cat","less","more"], default="cat")
    ashow.add_argument("--owner", help="Owner space (optional)")
    ashow.set_defaults(func=cmd_attach)

    # export
    s = sub.add_parser("export", help="Export events")
    sub_e = s.add_subparsers(dest="subcmd", required=True)
    ej = sub_e.add_parser("jsonl", help="Export as JSON Lines")
    ej.add_argument("--owner")
    ej.add_argument("--out", help="Output file (default stdout)")
    ej.set_defaults(func=cmd_export)

    return p


def main(argv: List[str] | None = None) -> None:
    parser = build_parser()
    ns = parser.parse_args(argv)
    set_color_mode(getattr(ns, "color", "auto"))
    ns.func(ns)


if __name__ == "__main__":  # pragma: no cover
    main(sys.argv[1:])
